/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview These functions define {Rect}s and {Size}s using two different coordinate spaces:
 *   1. Screenshot coords (SC suffix): where 0,0 is the top left of the screenshot image
 *   2. Display coords (DC suffix): that match the CSS pixel coordinate space of the LH report's page.
 */

import {Globals} from './report-globals.js';

/** @typedef {import('./dom.js').DOM} DOM */
/** @typedef {LH.Audit.Details.Rect} Rect */
/** @typedef {{width: number, height: number}} Size */

/**
 * @typedef InstallOverlayFeatureParams
 * @property {DOM} dom
 * @property {Element} rootEl
 * @property {Element} overlayContainerEl
 * @property {LH.Result.FullPageScreenshot} fullPageScreenshot
 */

/**
 * @param {LH.Result.FullPageScreenshot['screenshot']} screenshot
 * @param {LH.Audit.Details.Rect} rect
 * @return {boolean}
 */
function screenshotOverlapsRect(screenshot, rect) {
  return rect.left <= screenshot.width &&
    0 <= rect.right &&
    rect.top <= screenshot.height &&
    0 <= rect.bottom;
}

/**
 * @param {number} value
 * @param {number} min
 * @param {number} max
 */
function clamp(value, min, max) {
  if (value < min) return min;
  if (value > max) return max;
  return value;
}

/**
 * @param {Rect} rect
 */
function getElementRectCenterPoint(rect) {
  return {
    x: rect.left + rect.width / 2,
    y: rect.top + rect.height / 2,
  };
}

export class ElementScreenshotRenderer {
  /**
   * Given the location of an element and the sizes of the preview and screenshot,
   * compute the absolute positions (in screenshot coordinate scale) of the screenshot content
   * and the highlighted rect around the element.
   * @param {Rect} elementRectSC
   * @param {Size} elementPreviewSizeSC
   * @param {Size} screenshotSize
   */
  static getScreenshotPositions(elementRectSC, elementPreviewSizeSC, screenshotSize) {
    const elementRectCenter = getElementRectCenterPoint(elementRectSC);

    // Try to center clipped region.
    const screenshotLeftVisibleEdge = clamp(
      elementRectCenter.x - elementPreviewSizeSC.width / 2,
      0, screenshotSize.width - elementPreviewSizeSC.width
    );
    const screenshotTopVisisbleEdge = clamp(
      elementRectCenter.y - elementPreviewSizeSC.height / 2,
      0, screenshotSize.height - elementPreviewSizeSC.height
    );

    return {
      screenshot: {
        left: screenshotLeftVisibleEdge,
        top: screenshotTopVisisbleEdge,
      },
      clip: {
        left: elementRectSC.left - screenshotLeftVisibleEdge,
        top: elementRectSC.top - screenshotTopVisisbleEdge,
      },
    };
  }

  /**
   * Render a clipPath SVG element to assist marking the element's rect.
   * The elementRect and previewSize are in screenshot coordinate scale.
   * @param {DOM} dom
   * @param {HTMLElement} maskEl
   * @param {{left: number, top: number}} positionClip
   * @param {Rect} elementRect
   * @param {Size} elementPreviewSize
   */
  static renderClipPathInScreenshot(dom, maskEl, positionClip, elementRect, elementPreviewSize) {
    const clipPathEl = dom.find('clipPath', maskEl);
    const clipId = `clip-${Globals.getUniqueSuffix()}`;
    clipPathEl.id = clipId;
    maskEl.style.clipPath = `url(#${clipId})`;

    // Normalize values between 0-1.
    const top = positionClip.top / elementPreviewSize.height;
    const bottom = top + elementRect.height / elementPreviewSize.height;
    const left = positionClip.left / elementPreviewSize.width;
    const right = left + elementRect.width / elementPreviewSize.width;

    const polygonsPoints = [
      `0,0             1,0            1,${top}          0,${top}`,
      `0,${bottom}     1,${bottom}    1,1               0,1`,
      `0,${top}        ${left},${top} ${left},${bottom} 0,${bottom}`,
      `${right},${top} 1,${top}       1,${bottom}       ${right},${bottom}`,
    ];
    for (const points of polygonsPoints) {
      const pointEl = dom.createElementNS('http://www.w3.org/2000/svg', 'polygon');
      pointEl.setAttribute('points', points);
      clipPathEl.append(pointEl);
    }
  }

  /**
   * Called by report renderer. Defines a css variable used by any element screenshots
   * in the provided report element.
   * Allows for multiple Lighthouse reports to be rendered on the page, each with their
   * own full page screenshot.
   * @param {HTMLElement} el
   * @param {LH.Result.FullPageScreenshot['screenshot']} screenshot
   */
  static installFullPageScreenshot(el, screenshot) {
    el.style.setProperty('--element-screenshot-url', `url('${screenshot.data}')`);
  }

  /**
   * Installs the lightbox elements and wires up click listeners to all .lh-element-screenshot elements.
   * @param {InstallOverlayFeatureParams} opts
   */
  static installOverlayFeature(opts) {
    const {dom, rootEl, overlayContainerEl, fullPageScreenshot} = opts;
    const screenshotOverlayClass = 'lh-screenshot-overlay--enabled';
    // Don't install the feature more than once.
    if (rootEl.classList.contains(screenshotOverlayClass)) return;
    rootEl.classList.add(screenshotOverlayClass);

    // Add a single listener to the provided element to handle all clicks within (event delegation).
    rootEl.addEventListener('click', e => {
      const target = /** @type {?HTMLElement} */ (e.target);
      if (!target) return;
      // Only activate the overlay for clicks on the screenshot *preview* of an element, not the full-size too.
      const el = /** @type {?HTMLElement} */ (target.closest('.lh-node > .lh-element-screenshot'));
      if (!el) return;

      const overlay = dom.createElement('div', 'lh-element-screenshot__overlay');
      overlayContainerEl.append(overlay);

      // The newly-added overlay has the dimensions we need.
      const maxLightboxSize = {
        width: overlay.clientWidth * 0.95,
        height: overlay.clientHeight * 0.80,
      };

      const elementRectSC = {
        width: Number(el.dataset['rectWidth']),
        height: Number(el.dataset['rectHeight']),
        left: Number(el.dataset['rectLeft']),
        right: Number(el.dataset['rectLeft']) + Number(el.dataset['rectWidth']),
        top: Number(el.dataset['rectTop']),
        bottom: Number(el.dataset['rectTop']) + Number(el.dataset['rectHeight']),
      };
      const screenshotElement = ElementScreenshotRenderer.render(
        dom,
        fullPageScreenshot.screenshot,
        elementRectSC,
        maxLightboxSize
      );

      // This would be unexpected here.
      // When `screenshotElement` is `null`, there is also no thumbnail element for the user to have clicked to make it this far.
      if (!screenshotElement) {
        overlay.remove();
        return;
      }
      overlay.append(screenshotElement);
      overlay.addEventListener('click', () => overlay.remove());
    });
  }

  /**
   * Given the size of the element in the screenshot and the total available size of our preview container,
   * compute the factor by which we need to zoom out to view the entire element with context.
   * @param {Rect} elementRectSC
   * @param {Size} renderContainerSizeDC
   * @return {number}
   */
  static _computeZoomFactor(elementRectSC, renderContainerSizeDC) {
    const targetClipToViewportRatio = 0.75;
    const zoomRatioXY = {
      x: renderContainerSizeDC.width / elementRectSC.width,
      y: renderContainerSizeDC.height / elementRectSC.height,
    };
    const zoomFactor = targetClipToViewportRatio * Math.min(zoomRatioXY.x, zoomRatioXY.y);
    return Math.min(1, zoomFactor);
  }

  /**
   * Renders an element with surrounding context from the full page screenshot.
   * Used to render both the thumbnail preview in details tables and the full-page screenshot in the lightbox.
   * Returns null if element rect is outside screenshot bounds.
   * @param {DOM} dom
   * @param {LH.Result.FullPageScreenshot['screenshot']} screenshot
   * @param {Rect} elementRectSC Region of screenshot to highlight.
   * @param {Size} maxRenderSizeDC e.g. maxThumbnailSize or maxLightboxSize.
   * @return {Element|null}
   */
  static render(dom, screenshot, elementRectSC, maxRenderSizeDC) {
    if (!screenshotOverlapsRect(screenshot, elementRectSC)) {
      return null;
    }

    const tmpl = dom.createComponent('elementScreenshot');
    const containerEl = dom.find('div.lh-element-screenshot', tmpl);

    containerEl.dataset['rectWidth'] = elementRectSC.width.toString();
    containerEl.dataset['rectHeight'] = elementRectSC.height.toString();
    containerEl.dataset['rectLeft'] = elementRectSC.left.toString();
    containerEl.dataset['rectTop'] = elementRectSC.top.toString();

    // Zoom out when highlighted region takes up most of the viewport.
    // This provides more context for where on the page this element is.
    const zoomFactor = this._computeZoomFactor(elementRectSC, maxRenderSizeDC);

    const elementPreviewSizeSC = {
      width: maxRenderSizeDC.width / zoomFactor,
      height: maxRenderSizeDC.height / zoomFactor,
    };

    elementPreviewSizeSC.width = Math.min(screenshot.width, elementPreviewSizeSC.width);
    elementPreviewSizeSC.height = Math.min(screenshot.height, elementPreviewSizeSC.height);

    /* This preview size is either the size of the thumbnail or size of the Lightbox */
    const elementPreviewSizeDC = {
      width: elementPreviewSizeSC.width * zoomFactor,
      height: elementPreviewSizeSC.height * zoomFactor,
    };

    const positions = ElementScreenshotRenderer.getScreenshotPositions(
      elementRectSC,
      elementPreviewSizeSC,
      {width: screenshot.width, height: screenshot.height}
    );

    const imageEl = dom.find('div.lh-element-screenshot__image', containerEl);
    imageEl.style.width = elementPreviewSizeDC.width + 'px';
    imageEl.style.height = elementPreviewSizeDC.height + 'px';

    imageEl.style.backgroundPositionY = -(positions.screenshot.top * zoomFactor) + 'px';
    imageEl.style.backgroundPositionX = -(positions.screenshot.left * zoomFactor) + 'px';
    imageEl.style.backgroundSize =
      `${screenshot.width * zoomFactor}px ${screenshot.height * zoomFactor}px`;

    const markerEl = dom.find('div.lh-element-screenshot__element-marker', containerEl);
    markerEl.style.width = elementRectSC.width * zoomFactor + 'px';
    markerEl.style.height = elementRectSC.height * zoomFactor + 'px';
    markerEl.style.left = positions.clip.left * zoomFactor + 'px';
    markerEl.style.top = positions.clip.top * zoomFactor + 'px';

    const maskEl = dom.find('div.lh-element-screenshot__mask', containerEl);
    maskEl.style.width = elementPreviewSizeDC.width + 'px';
    maskEl.style.height = elementPreviewSizeDC.height + 'px';

    ElementScreenshotRenderer.renderClipPathInScreenshot(
      dom,
      maskEl,
      positions.clip,
      elementRectSC,
      elementPreviewSizeSC
    );

    return containerEl;
  }
}
